// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.

// This code has been ported almost directly from Go's src/bytes/buffer.go
// Copyright 2009 The Go Authors. All rights reserved. BSD license.
// https://github.com/golang/go/blob/master/LICENSE
"use strict";

((window) => {
  const { assert } = window.__bootstrap.infra;
  const {
    TypedArrayPrototypeSubarray,
    TypedArrayPrototypeSlice,
    TypedArrayPrototypeSet,
    MathFloor,
    MathMin,
    PromiseResolve,
    Uint8Array,
    Error,
  } = window.__bootstrap.primordials;

  // MIN_READ is the minimum ArrayBuffer size passed to a read call by
  // buffer.ReadFrom. As long as the Buffer has at least MIN_READ bytes beyond
  // what is required to hold the contents of r, readFrom() will not grow the
  // underlying buffer.
  const MIN_READ = 32 * 1024;
  const MAX_SIZE = 2 ** 32 - 2;

  // `off` is the offset into `dst` where it will at which to begin writing values
  // from `src`.
  // Returns the number of bytes copied.
  function copyBytes(src, dst, off = 0) {
    const r = dst.byteLength - off;
    if (src.byteLength > r) {
      src = TypedArrayPrototypeSubarray(src, 0, r);
    }
    TypedArrayPrototypeSet(dst, src, off);
    return src.byteLength;
  }

  class Buffer {
    #buf = null; // contents are the bytes buf[off : len(buf)]
    #off = 0; // read at buf[off], write at buf[buf.byteLength]

    constructor(ab) {
      if (ab == null) {
        this.#buf = new Uint8Array(0);
        return;
      }

      this.#buf = new Uint8Array(ab);
    }

    bytes(options = { copy: true }) {
      if (options.copy === false) {
        return TypedArrayPrototypeSubarray(this.#buf, this.#off);
      }
      return TypedArrayPrototypeSlice(this.#buf, this.#off);
    }

    empty() {
      return this.#buf.byteLength <= this.#off;
    }

    get length() {
      return this.#buf.byteLength - this.#off;
    }

    get capacity() {
      return this.#buf.buffer.byteLength;
    }

    truncate(n) {
      if (n === 0) {
        this.reset();
        return;
      }
      if (n < 0 || n > this.length) {
        throw Error("bytes.Buffer: truncation out of range");
      }
      this.#reslice(this.#off + n);
    }

    reset() {
      this.#reslice(0);
      this.#off = 0;
    }

    #tryGrowByReslice(n) {
      const l = this.#buf.byteLength;
      if (n <= this.capacity - l) {
        this.#reslice(l + n);
        return l;
      }
      return -1;
    }

    #reslice(len) {
      assert(len <= this.#buf.buffer.byteLength);
      this.#buf = new Uint8Array(this.#buf.buffer, 0, len);
    }

    readSync(p) {
      if (this.empty()) {
        // Buffer is empty, reset to recover space.
        this.reset();
        if (p.byteLength === 0) {
          // this edge case is tested in 'bufferReadEmptyAtEOF' test
          return 0;
        }
        return null;
      }
      const nread = copyBytes(
        TypedArrayPrototypeSubarray(this.#buf, this.#off),
        p,
      );
      this.#off += nread;
      return nread;
    }

    read(p) {
      const rr = this.readSync(p);
      return PromiseResolve(rr);
    }

    writeSync(p) {
      const m = this.#grow(p.byteLength);
      return copyBytes(p, this.#buf, m);
    }

    write(p) {
      const n = this.writeSync(p);
      return PromiseResolve(n);
    }

    #grow(n) {
      const m = this.length;
      // If buffer is empty, reset to recover space.
      if (m === 0 && this.#off !== 0) {
        this.reset();
      }
      // Fast: Try to grow by means of a reslice.
      const i = this.#tryGrowByReslice(n);
      if (i >= 0) {
        return i;
      }
      const c = this.capacity;
      if (n <= MathFloor(c / 2) - m) {
        // We can slide things down instead of allocating a new
        // ArrayBuffer. We only need m+n <= c to slide, but
        // we instead let capacity get twice as large so we
        // don't spend all our time copying.
        copyBytes(TypedArrayPrototypeSubarray(this.#buf, this.#off), this.#buf);
      } else if (c + n > MAX_SIZE) {
        throw new Error("The buffer cannot be grown beyond the maximum size.");
      } else {
        // Not enough space anywhere, we need to allocate.
        const buf = new Uint8Array(MathMin(2 * c + n, MAX_SIZE));
        copyBytes(TypedArrayPrototypeSubarray(this.#buf, this.#off), buf);
        this.#buf = buf;
      }
      // Restore this.#off and len(this.#buf).
      this.#off = 0;
      this.#reslice(MathMin(m + n, MAX_SIZE));
      return m;
    }

    grow(n) {
      if (n < 0) {
        throw Error("Buffer.grow: negative count");
      }
      const m = this.#grow(n);
      this.#reslice(m);
    }

    async readFrom(r) {
      let n = 0;
      const tmp = new Uint8Array(MIN_READ);
      while (true) {
        const shouldGrow = this.capacity - this.length < MIN_READ;
        // read into tmp buffer if there's not enough room
        // otherwise read directly into the internal buffer
        const buf = shouldGrow
          ? tmp
          : new Uint8Array(this.#buf.buffer, this.length);

        const nread = await r.read(buf);
        if (nread === null) {
          return n;
        }

        // write will grow if needed
        if (shouldGrow) {
          this.writeSync(TypedArrayPrototypeSubarray(buf, 0, nread));
        } else this.#reslice(this.length + nread);

        n += nread;
      }
    }

    readFromSync(r) {
      let n = 0;
      const tmp = new Uint8Array(MIN_READ);
      while (true) {
        const shouldGrow = this.capacity - this.length < MIN_READ;
        // read into tmp buffer if there's not enough room
        // otherwise read directly into the internal buffer
        const buf = shouldGrow
          ? tmp
          : new Uint8Array(this.#buf.buffer, this.length);

        const nread = r.readSync(buf);
        if (nread === null) {
          return n;
        }

        // write will grow if needed
        if (shouldGrow) {
          this.writeSync(TypedArrayPrototypeSubarray(buf, 0, nread));
        } else this.#reslice(this.length + nread);

        n += nread;
      }
    }
  }

  async function readAll(r) {
    const buf = new Buffer();
    await buf.readFrom(r);
    return buf.bytes();
  }

  function readAllSync(r) {
    const buf = new Buffer();
    buf.readFromSync(r);
    return buf.bytes();
  }

  async function writeAll(w, arr) {
    let nwritten = 0;
    while (nwritten < arr.length) {
      nwritten += await w.write(TypedArrayPrototypeSubarray(arr, nwritten));
    }
  }

  function writeAllSync(w, arr) {
    let nwritten = 0;
    while (nwritten < arr.length) {
      nwritten += w.writeSync(TypedArrayPrototypeSubarray(arr, nwritten));
    }
  }

  window.__bootstrap.buffer = {
    writeAll,
    writeAllSync,
    readAll,
    readAllSync,
    Buffer,
  };
})(this);
